﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Security.Permissions;
using System.Runtime.Serialization;

namespace SpeakupBackend.Themes
{
    [Serializable]
    public sealed class Week: IEquatable<Week>, 
                              IComparable, 
                              IComparable<Week>,
                              ISerializable
    {
        public const int MinValidWeekNumber = 1;
        public const int MaxValidWeekNumber = 12;

        public static bool IsValidWeekNumber(int weekNumber)
        {
            return (weekNumber >= MinValidWeekNumber && weekNumber <= MaxValidWeekNumber);
        }

        static List<Week> AvailableWeeks = new List<Week>();
        static Week()
        {
            AvailableWeeks.Capacity = MaxValidWeekNumber - MinValidWeekNumber + 1;
            for (int i = MinValidWeekNumber; i <= MaxValidWeekNumber; i++)
            {
                AvailableWeeks.Add(new Week(i));
            }
        }

        public static Week GetWeek(int weekNumber)
        {
            if (!IsValidWeekNumber(weekNumber))
            {
                throw new ArgumentException("WeekNumber");
            }
            return AvailableWeeks[weekNumber - 1];
        }

        public static Week GetWeek(string strWeekNumber)
        {
            int weekNumber;
            if (!Int32.TryParse(strWeekNumber, NumberStyles.Integer, CultureInfo.InvariantCulture, out weekNumber) )
            {
                throw new FormatException(String.Format("Cant parse {0} as week number!",strWeekNumber));
            }

            return GetWeek(weekNumber);
        }

        public static IList<Week> GetWeeks()
        {
            return AvailableWeeks.AsReadOnly();
        }
        
        readonly int m_weekNumber;
        public int WeekNumber
        {
            get
            {
                return m_weekNumber;
            }
        }

        private Week(int weekNumber)            
        {
            if (!IsValidWeekNumber(weekNumber))
            {
                throw new ArgumentException("WeekNumber");
            }
            m_weekNumber = weekNumber;
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as Week);
        }

        public bool Equals(Week other)
        {
            return ClassHelper.IEquatableEquals(this, other);
        }

        public override int GetHashCode()
        {
            return WeekNumber.GetHashCode();
        }

        public override string ToString()
        {
            return WeekNumber.ToString();
        }

        public int CompareTo(object obj)
        {
            return CompareTo((Week)obj);
        }

        public int CompareTo(Week other)
        {
            return WeekNumber - other.WeekNumber;
        }

        #region 'Get Week By Date' functionality

        /// <summary>
        /// Known date and it's week number
        /// </summary>
        static public readonly DateTime KnownDate = new DateTime(2010, 4, 26);
        static public readonly int KnownWeek = 9;

        /// <summary>
        /// Returns date that represents monday of the week
        /// specified by @param date
        /// </summary>
        /// <param name="date"></param>
        /// <returns>Date of the monday of the week specified by @param date</returns>
        private static DateTime MoveToMonday(DateTime date)
        {
            if (date.DayOfWeek == DayOfWeek.Monday) return date;

            int curDay = (int)date.DayOfWeek;
            if (curDay == 0) curDay = 7;

            int deltaDays = curDay - (int)DayOfWeek.Monday;
            return date - TimeSpan.FromDays(deltaDays);
        }

        /// <summary>
        /// Returns date that represents sunday of the week
        /// specified by @param date
        /// </summary>
        /// <param name="date"></param>
        /// <returns>Date of the sunday of the week specified by @param date</returns>
        private static DateTime MoveToSunday(DateTime date)
        {
            if (date.DayOfWeek == DayOfWeek.Sunday) return date;

            int curDay = (int)date.DayOfWeek;
            if (curDay == 0) curDay = 7;

            int deltaDays = 7 - curDay;
            return date + TimeSpan.FromDays(deltaDays);
        }

        public static Week GetWeek(DateTime requestDate)
        {
            return GetWeek(KnownDate, KnownWeek, requestDate);
        }

        public static Week GetWeek(DateTime knownDate, int knownWeek, DateTime requestDate)
        {
            if (knownDate < requestDate)
            {
                knownDate = MoveToMonday(knownDate);
                requestDate = MoveToMonday(requestDate);
            }
            else
            {
                knownDate = MoveToSunday(knownDate);
                requestDate = MoveToSunday(requestDate);
            }

            int deltaDays = (int)(requestDate - knownDate).TotalDays;
            int deltaWeeks = deltaDays / 7;

            int week = (knownWeek + deltaWeeks) % MaxValidWeekNumber;

            if (week < 0) week += MaxValidWeekNumber;
            if (week == 0) week = MaxValidWeekNumber;

            return GetWeek(week);
        }

        static public Week GetCurrentWeek()
        {
            return GetWeek(KnownDate, KnownWeek, DateTime.Now);            
        }

        #endregion

        #region Comparison operator

        static public bool operator == (Week rhs, Week lhs)
        {
            return ClassHelper.OperatorEqual(rhs, lhs);
        }

        static public bool operator !=(Week rhs, Week lhs)
        {
            return ClassHelper.OperatorNotEqual(rhs, lhs);
        }

        static public bool operator >(Week rhs, Week lhs)
        {
            return rhs.CompareTo(lhs) > 0;
        }

        static public bool operator < (Week rhs, Week lhs)
        {
            return rhs.CompareTo(lhs) < 0;
        }

        static public bool operator >=(Week rhs, Week lhs)
        {
            return rhs.CompareTo(lhs) >= 0;
        }

        static public bool operator <=(Week rhs, Week lhs)
        {
            return rhs.CompareTo(lhs) <= 0;
        }

        #endregion

        #region Serialization
        static readonly string WeekNumberSerializationKey = "WeekNumber";

        [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.SetType(typeof(SerializationHelper));
            info.AddValue(WeekNumberSerializationKey, WeekNumber);
        }

        [Serializable]
        private sealed class SerializationHelper : IObjectReference, ISerializable
        {
            int WeekNumber;

            SerializationHelper(SerializationInfo info, StreamingContext context)
            {
                WeekNumber = info.GetInt32(WeekNumberSerializationKey);
            }

            Object IObjectReference.GetRealObject(StreamingContext context)
            {
                return Week.GetWeek(WeekNumber);
            }

            void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
            {
                throw new NotImplementedException();
            }
        }
        #endregion
    }
}
